#include <iostream>
#include "Common.hpp"
#include "Socket.hpp"
#include <sys/select.h>

using namespace myLog;
using namespace mySocket;

class SelectServer{
public:
    const static int size = sizeof(fd_set) * 8;
    const static int defaultfd = -1;
private:


public:
    SelectServer(uint16_t port)
        :_port(port),
        _is_running(false),
        _listensock(std::make_unique<TcpSocket>())
    {
        _listensock->CreateServerTcpSocket(_port);
        //初始化辅助数组，-1代表是不合法的fd
        for(int i = 0; i < size; ++i) _auxiliary_array[i] = defaultfd;
        //规定，0号下标的是监听fd
        _auxiliary_array[0] = _listensock->GetFd();
    }
    ~SelectServer(){}

    void Start(){
        _is_running = true;
        while(_is_running){
            fd_set rfds;    // 此次用于接收所有需要关注读事件的文件描述符集合
            FD_ZERO(&rfds); // 清空一下rfds
            int maxfd = -1;
            
            // 1.先把辅助数组内所有的套接字给导入到一个fd_set内
            for(int i = 0; i < size; ++i){
                if(_auxiliary_array[i] == defaultfd) continue;
                //剩下的就可以导入到fd_set内
                FD_SET(_auxiliary_array[i], &rfds);
                if(maxfd < _auxiliary_array[i]) maxfd = _auxiliary_array[i];
            }
            
            //Debug 测试 打印辅助数组的fd
            PrintFdArray();
            std::cout << maxfd << std::endl;

            //注意，这里在写其他服务器的时候，上来就直接accept了！
            //但在使用select多路转接的时候，是不行的！因为accept会默认阻塞！
            //所以，阻塞的事情应该交给select来做，怎么办呢？ -> 所谓的accept获取链接，本质是在监听套接字的读事件！
            //这个地方逻辑是比较混乱的，我这里直接写最终版本了
            //具体的问题放在blog上来说！！！

            // 2.现在就得到了一个需要被关注的文件描述符集合！ 
            // 但是，rfds是一个开辟在栈上的一个局部变量，类似于信号表，所以需要设置到内核

            // 使用select的时候，最后一个参数是设置超时时间的
            // 如果正常设置，代表等待这么长时间; 如果是{0，0}，非阻塞轮询; 如果是nullptr，代笔阻塞   
            //timeval tv = {2, 0};
            //timeval tv = {0, 0};
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr); 
            // 这里select就只是为了验证接口的使用，写一个简单的echo server
            // 所以，就不管写事件和异常事件了！！！

            //select有三种返回值
            // <0 出错； =0 等待超时  >0说明有文件描述符对应的事件就绪了，返回的值是就绪文件描述符的个数！
            if(n < 0){
                LOG(LogLevel::ERROR) << "select error!";
                exit(SELECT_ERROR);
            }
            else if(n == 0){
                LOG(LogLevel::INFO) << "select timeout!";
                continue;
            }
            // 当 n > 0 的时候，说明此时已就绪的文件描述符就已经被设置到了rfds下！
            // 但因为后面获取链接后，新的文件描述符也要被select接
            // 所以这里其实是不知道就绪的文件描述符是listensockfd，还是sockfd！！！
            else{
                // 但是没有关系，我们这里不进行处理，递交给 <事件派发器> 处理
                Dispatcher(rfds);
            }
        }
        _is_running = false;
    }

    void Dispatcher(fd_set rfds /*当然未来也可以加入另外两个位图进行操作！*/){
        for(int i = 0; i < size; ++i){
            // select返回来的fd_set:
            // 位置代表的是文件描述符，如果值为1代表就绪，反之不就绪！
            
            //fd不合法
            if(_auxiliary_array[i] == defaultfd) continue;
            
            // 合法但是不一定就绪
            // 就绪了↓
            if(FD_ISSET(_auxiliary_array[i], &rfds)){
                //问题是，就绪了也不太清楚，到底是链接就绪，还是普通套接字的就绪？

                // 链接就绪
                if(_auxiliary_array[i] == _listensock->GetFd()) Accepter();
                // 普通套接字读就绪
                else Recver(i);
            }
            //下面还可以针对于不同的fd_set做操作！这里不写了！
        }
    }

    // 链接管理器——链接管理 (把链接设置到内核)
    void Accepter(){
        InetAddr client;
        int sockfd = _listensock->AcceptOrDie(&client);
        
        // 在之前是一获取就直接进行read or recv 读取！
        // 但是，默认是阻塞的！ 我们这里是要等和IO操作分离的！
        // 所以，新获得的文件描述符，不一定能够马上读取 -> 交给select去接管
    
        // 如何接管？
        // 直接添加到辅助数组中！
        // 退出该函数的时候，会重新进行select等待，在select之前，会把辅助数组内的合法文件描述符导入到fd_set内！
        int pos = 0;  
        for(pos = 0; pos < size; ++pos)
            if(_auxiliary_array[pos] == defaultfd) break;
        
        //但是出来的时候，pos可能是合法位置，也可能是size位置
        if(pos == size) {
            LOG(LogLevel::WARNING) << "select server full";
            close(sockfd);
        }
        else _auxiliary_array[pos] = sockfd;
    }

    // IO处理器——这里只有读
    // 这里只需要把辅助数组中，文件描述符 存储位置下标拿过来即可
    void Recver(int index){
        char buffer[1024] = {0};
        int n = recv(_auxiliary_array[index], buffer, sizeof(buffer) - 1, 0);
        if(n < 0){
            // 读取出错了！
            LOG(LogLevel::ERROR) << "read error";
            close(_auxiliary_array[index]);
            _auxiliary_array[index] = defaultfd;
        }
        else if(n == 0){
            // 客户端退出
            LOG(LogLevel::ERROR) << "client quit";
            close(_auxiliary_array[index]);
            _auxiliary_array[index] = defaultfd;
        }
        else{
            // 正常读取
            buffer[n] = 0;
            // 这里是echo server 直接打印即可
            LOG(LogLevel::INFO) << "client says@ " << buffer;
        }
    }

    void Stop(){_is_running = false;}

    void PrintFdArray(){
        for(int i = 0; i < size; ++i){
            if(_auxiliary_array[i] == -1) continue;
            else std::cout << _auxiliary_array[i] << " ";
        }
        std::cout << std::endl;
    }

private:
    uint16_t _port;
    bool _is_running;
    std::unique_ptr<Socket> _listensock;

    //辅助数组 -> select服务器使用的！
    int _auxiliary_array[size];
};